package br.com.felix.fwt.ui;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.jfree.chart.ChartUtilities;
import org.jfree.chart.JFreeChart;

import br.com.felix.fwt.error.ApplicationError;
import br.com.felix.fwt.log.LoggerFactory;
import br.com.felix.fwt.security.Profile;
import br.com.felix.fwt.security.exception.UnauthorizedAccessException;
import br.com.felix.fwt.servlet.FelixWebToolkitServlet;
import br.com.felix.fwt.servlet.exception.FormNotFoundException;
import br.com.felix.fwt.servlet.exception.UpdateFormFieldsException;
import br.com.felix.fwt.svg.SVGDocument;
import br.com.felix.fwt.ui.css.ExternalCSS;
import br.com.felix.fwt.ui.css.Style;
import br.com.felix.fwt.ui.exception.ChartNotFoundException;
import br.com.felix.fwt.ui.exception.ComponentRenderException;
import br.com.felix.fwt.ui.exception.SVGNotFoundException;
import br.com.felix.fwt.ui.exception.UserInterfaceInitializationException;
import br.com.felix.fwt.ui.javascript.ExternalJavascript;
import br.com.felix.fwt.ui.javascript.Javascript;
import br.com.felix.fwt.util.IdGenerator;

/**
 * The Class Form is the most important class of the framework. This class is
 * used to define the web user interfaces. We can say that this is our "JFrame".
 */
public abstract class Form extends BaseComponent implements Serializable {

	
	private static final long serialVersionUID = -7716033046336042970L;


	/** The ENCTYPE of the form. */
	private EncType encType;
	
	/** The hashmap actions is used to quickly access the ActionListeners. */
	private Map<String, ActionListener> hsmActions;
	
	/** The hashmap components. */
	private Map<String, BaseComponent> hsmComponents;
	
	/** The list of javascripts to be run when form finishes loading. */
	private List<Javascript> javascript;
	
	/** The list of javascripts associated with a component to be run when form finishes loading. */
    private Map<BaseComponent, Javascript[]> componentJavascript;
    
    private Map<String, String> hsmParameters;
	
	/**
	 * The list of javascripts to be run when form finishes loading. This list
	 * will be empty after execution ("write" method is called).
	 */
	private List<Javascript> runOnceJavascripts;
	
	/** The styles to be declared at the HTML heading. */
	private List<Style> declaredStyles;
	
	/** The referenced external styles. */
	private List<ExternalCSS> referencedStyles;
	
	/** The referenced external javascripts. */
	private List<ExternalJavascript> referencedJavascripts;
	
	private static Logger logger = LoggerFactory.getLogger(Form.class);
	
	/** List of validation errors encountered in the form. */
	protected ApplicationErrorList applicationErrorList = new ApplicationErrorList();


	/**
	 * Instantiates a new form. Defines the standard formAction. 
	 * Defines the method as POST.
	 * Initializes internal variables.
	 */
	public Form(EncType encType) {
		hsmActions = new HashMap<String, ActionListener>();
		hsmComponents = new HashMap<String, BaseComponent>();
		hsmParameters = new HashMap<String, String>();
		this.encType = encType;
	}



	/**
	 * Returns the form action. Follows framework convention.
	 * */
	protected String getFormAction() {
		StringBuilder strb = new StringBuilder(FelixWebToolkitServlet.formAction(getClass()));
		if (!hsmParameters.isEmpty()){
			strb.append("?");
			for (String p: hsmParameters.keySet()){
				strb.append(p).append("=").append(hsmParameters.get(p)).append("&");
			}
			strb.delete(strb.length()-1, strb.length());
		}
		return strb.toString();
	}
	
	
	
	/**
	 * Sets enctype to APPLICATION_X_WWW_FORM_URLENCODED.
	 * @see #Form(EncType)
	 * */
	public Form(){
	    this(EncType.APPLICATION_X_WWW_FORM_URLENCODED);
	}
	
	
	


	/**
	 * Returns false if any child's validate method return false. True otherwise.
	 * If there are any validation errors, they will be added to the validationErrorList.
	 * */
	@Override
	public boolean validate() {
		applicationErrorList.clear();
		List<ApplicationError> lstErrors = new ArrayList<ApplicationError>();
		boolean validate = true;
		for(BaseComponent child:childs){
			if(!child.validate()){
				validate = false;
				lstErrors.addAll(child.getValidationErrors());
			}
		}
		if(!validate)applicationErrorList.addApplicationErrors(lstErrors);
		return validate;
	}

	

	/**
	 * Returns all of the childs' validation errors.
	 * */
	@Override
	public List<ApplicationError> getValidationErrors() {
		List<ApplicationError> lstErrors = new ArrayList<ApplicationError>();
		for(BaseComponent child:childs){
			if(!child.validate()){
				lstErrors.addAll(child.getValidationErrors());
			}
		}
		return lstErrors;
	}



	/**
	 * This method navigates the component tree, registering components that
	 * have listeners.
	 */
	public void registerComponents() {
		hsmActions.clear();
		hsmComponents.clear();
		IdGenerator idGen = new IdGenerator();
		registerComponents(childs, this, idGen);
	}
	


	/**
	 * Recursive method to navigate the component tree, registering components
	 * that have ActionListeners.
	 * 
	 * @param childs the childs
	 */
	private void registerComponents(List<BaseComponent> childs, BaseComponent parent, IdGenerator idGen) {
		if(childs!=null){
			for (BaseComponent child : childs) {
			    if (child == null){
	                logger.warn("Form '" + getClass().getName()+ "' added null child to component. Parent component is '" + parent.getName() + "'.");
	            }
			    else{
			    	child.generateName(idGen);
    				if (child.actionListener != null) {
    					child.actionListener.generateName(idGen);
    					logger.debug("Registered action named '" + child.actionListener.actionName + "'");
    					hsmActions.put(child.actionListener.actionName, child.actionListener);
    				}
    				if (child.isInputControl()) {
    				    logger.debug("Registered input named '" + child.getName() + "'.");
    					hsmComponents.put(child.getName(), child);
    				}
    				if (child.childs != null){
    					registerComponents(child.childs, child, idGen);
    				}
			    }
			}
		}
		else{
			logger.warn("Form with no childs.");
		}
	}
	


	/*
	 * (non-Javadoc)
	 * 
	 * @see br.com.felix.fwt.ui.BaseComponent#write(java.io.PrintWriter)
	 */
	@Override
	public void write(PrintWriter out) throws ComponentRenderException {
		if(!authorized()) return;
		out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
		out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n\t");
		out.write("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n");
		out.write("<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"pt\" lang=\"pt\">\n");
		out.write("<head>\n<title>");
		out.write(getTitle());
		out.write("</title>\n");
		if (referencedStyles != null) {
			for (ExternalCSS c : referencedStyles) {
				c.write(out);
			}
		}
		if (referencedJavascripts != null) {
			for (ExternalJavascript c : referencedJavascripts) {
				c.write(out);
			}
		}
		if (declaredStyles != null) {
			out.write("\n<style type=\"text/css\">");
			for (Style s : declaredStyles) {
				out.write(s.declaration());
			}
			out.write("\n</style>");
		}
		out.write("\n</head>\n<body ");
		out.write(WebsiteStyle.websiteStyle);
		out.write(">\n<form");
		out.write(" name=\"");
		out.write(getName());
		out.write("\" ");

		out.write(" method=\"post\" ");
		
		if (encType != null) {
			out.write(" ENCTYPE=\"");
			out.write(encType.toString());
			out.write("\" ");
		}
		out.write(" action=\"");
		out.write(getFormAction());
		out.write("\">\n");
		out.write("\n<input type=\"hidden\" name=\"actionName\" value=\"\" />");
		writeChilds(out);
		out.write("\n\n</form>\n");
		if (this.javascript != null || this.runOnceJavascripts != null) {
			out.write("\n<script type=\"text/javascript\">");
			if (this.javascript != null) {
				for (Javascript j : javascript) {
					out.write(j.script());
				}
			}
			if (this.componentJavascript != null) {
                for (Javascript[] js : componentJavascript.values()) {
                    for(Javascript j : js){
                        out.write(j.script());
                    }
                }
            }
			if (runOnceJavascripts != null) {
				for (Javascript j : runOnceJavascripts) {
					out.write(j.script());
				}
				runOnceJavascripts.clear();
			}
			out.write("\n</script>");
		}
		out.write("\n</body>\n</html>");
	}
	


	/**
	 * Calls the appropriate ActionListener for the requested action.
	 * 
	 * @param action the action fired by the user interface.
	 * @return the form to be presented to the user after the action execution.
	 * @throws FormNotFoundException 
	 */
	public Form executeAction(String action) throws FormNotFoundException {
		ActionListener actionListener = hsmActions.get(action);
		if (actionListener == null)
			throw new RuntimeException("Action does not exist: '" + action + "'.");
		Form nextForm = null;
		try {
			nextForm = actionListener.actionPerformed();
		}
		catch (UserInterfaceInitializationException e) {
			logger.error("Error initializing user interface during action '"+action+"'.",e);
			nextForm = catchUserInterfaceInitializationException(e);
		}
		catch (UnauthorizedAccessException e) {
			logger.error("Unauthorized access during action '"+action+"'.",e);
			nextForm = catchUnauthorizedAccessException(e);
		}
		if (nextForm == null){
			throw new FormNotFoundException("Action '" + action + "' did not return the next form.");
		}
		if (actionListener.changesInterfacesComponents){
			nextForm.registerComponents();
		}
		return nextForm;
	}
	

	/**
	 * Returns the next Form to be executed when this has failed to execute.
	 * */
	public abstract Form catchUserInterfaceInitializationException(UserInterfaceInitializationException e);
	
	public abstract Form catchUpdateFormFieldsException(UpdateFormFieldsException e);
	
	/**
	 * Returns the next Form to be executed when this has failed to execute.
	 * */
	public abstract Form catchUnauthorizedAccessException(UnauthorizedAccessException e);

	/**
	 * Obtains input from the request and updates the components' values.
	 * */
	public String updateFormFields(HttpServletRequest request) throws UpdateFormFieldsException{
		String action = request.getParameter("actionName");
		Enumeration<String> parameters = request.getParameterNames();
		while (parameters.hasMoreElements()) {
			String paramName = parameters.nextElement();
			updateComponentValue(paramName, request.getParameter(paramName));
		}
		return action;
	}
	
	/**
	 * Updates a component value. This method is used by the framework to update
	 * the components' values after the form is submitted.
	 * 
	 * @param componentName the component name
	 * @param newValue the new value
	 */
	public void updateComponentValue(String componentName, String newValue) {
		BaseComponent component = hsmComponents.get(componentName);
		if (component != null) {
			component.setValue(newValue);
		}
		else {
			if (!"actionName".equals(componentName) && !"svg".equals(componentName) && !"chart".equals(componentName) && !hsmParameters.containsKey(componentName)) {
				logger.warn("Unable to update component value. Component '" + componentName + "' is not in the hash.");
			}
		}
	}
	


	/**
	 * Adds the javascript to the list of javascripts to be run when the form is
	 * loaded.
	 * 
	 * @param javascript the javascript to be run when the form is loaded.
	 */
	public void executeJavascript(Javascript javascript) {
		if (this.javascript == null){
			this.javascript = new ArrayList<Javascript>();
		}
		this.javascript.add(javascript);
	}
	
	
	/**
     * Adds the javascript to the list of javascripts to be run when the form is
     * loaded.
     * 
     * @param javascript the javascript to be run when the form is loaded.
     */
    public void executeJavascriptOfComponent(BaseComponent component, Javascript ... javascript) {
        if (this.componentJavascript == null){
            this.componentJavascript = new LinkedHashMap<BaseComponent,Javascript[]>();
        }
        this.componentJavascript.put(component, javascript);
    }
	


	/**
	 * Adds the javascript to the list of javascripts to be run when the form is
	 * loaded. The javascript is run only once. After the current request, it
	 * will not execute again.
	 * 
	 * @param javascript the javascript to be run when the form is loaded.
	 */
	public void executeJavascriptOnce(Javascript javascript) {
		if (this.runOnceJavascripts == null)
			this.runOnceJavascripts = new ArrayList<Javascript>();
		this.runOnceJavascripts.add(javascript);
	}
	


	/**
	 * Declare style.
	 * 
	 * @param stylethe style
	 */
	public void declareStyle(Style style) {
		if (this.declaredStyles == null)
			this.declaredStyles = new ArrayList<Style>();
		this.declaredStyles.add(style);
	}
	


	/**
	 * Reference style.
	 * 
	 * @param style the style
	 */
	public void referenceStyle(ExternalCSS style) {
		if (this.referencedStyles == null)
			this.referencedStyles = new ArrayList<ExternalCSS>();
		this.referencedStyles.add(style);
	}
	
	
	/**
	 * Reference javascript.
	 * 
	 * @param script the script
	 */
	public void referenceJavascript(ExternalJavascript script) {
		if (this.referencedJavascripts == null)
			this.referencedJavascripts = new ArrayList<ExternalJavascript>();
		this.referencedJavascripts.add(script);
	}


	/**
	 * Initialize user interface. This method is where you put your code to
	 * construct the user interface. You could put it in the constructor, but if
	 * you desire to access the FwtContext, it will cause an error.
	 * 
	 * @throws UserInterfaceInitializationException when an occurs that prevents
	 * the user interface to initialize correctly.
	 */
	protected abstract void initializeUserInterface() throws UserInterfaceInitializationException;
	
	

	/**
	 * Makes security verification, and initializes the user interface.
	 * @throws UserInterfaceInitializationException 
	 * @throws UnauthorizedAccessException 
	 * */
	public void initializeForm() throws UserInterfaceInitializationException, UnauthorizedAccessException{
		Profile requiredProfile = getRequiredProfile();
		if(requiredProfile!=null){
			Profile userProfile = FelixWebToolkitServlet.getUserProfile();
			boolean authorized = userProfile!=null && userProfile.authorizedWhenRequiredProfileIs(requiredProfile);
			if(!authorized){
				throw new UnauthorizedAccessException("User is not authorized to access the resource.");
			}
		}
		initializeUserInterface();
	} 

	

	/**
	 * Gets the title for the page.
	 */
	public abstract String getTitle();
	
	
	protected <T extends Form> T getLoadedForm(Class<T> clazz){
		return FelixWebToolkitServlet.getLoadedForm(clazz);
	}



	public void writeSVG(String svgComponent, PrintWriter printWriter) throws SVGNotFoundException {
		Object component = hsmComponents.get(svgComponent);
		if(component==null){
			throw new SVGNotFoundException("Requested SVG was not found: "+svgComponent);
		}
		else{
			SVGLink svgLink = (SVGLink)component;
			SVGDocument svgDocument = svgLink.getSVGDocument();
			svgDocument.write(printWriter);
		}
	}



    public void writeChart(String chartComponent, OutputStream os) throws ChartNotFoundException, IOException {
        Object component = hsmComponents.get(chartComponent);
        if(component==null){
            throw new ChartNotFoundException("Requested CHART was not found: "+chartComponent);
        }
        else{
            Chart chart = (Chart)component;
            JFreeChart jfreeChart = chart.getChart();
            ChartUtilities.writeChartAsPNG(os, jfreeChart, chart.getWidth(), chart.getHeight());
        }
    }



    @Override
    public void remove(BaseComponent child) {
        if(componentJavascript!=null){
            componentJavascript.remove(child);
        }
        super.remove(child);
    }



    /**
     * Executed when the user requests a URL directly from the browser (not clicking and dispatching a user action).
     * */
    public abstract void reload()throws UserInterfaceInitializationException;

    
    /**
	 * Obtains a unique name to identify a form within the framework.
	 * */
	public String getName() {
		return getClass().getCanonicalName();
	}


	/**
	 * Add a parameter to the parameter list.
	 * */
	public void addParameter(String name) {
		if (name == null || name.length() == 0) {
			String msg = String.format("Invalid parameter name.", name);
			logger.error(msg);
			throw new InvalidParameterException(msg);
		}
		hsmParameters.put(name, "");
	}



	/**
	 * Sets a parameter value.
	 * */
	public void setParameter(String name, String value) {
		if (!hsmParameters.containsKey(name)) {
			String msg = String.format("Parameter '%s' was not declared.", name);
			logger.error(msg);
			throw new InvalidParameterException(msg);
		}
		hsmParameters.put(name, value);
	}


	/**
	 * Gets the value of the specified parameter.
	 * */
	public String getParameter(String name) {
		if (!hsmParameters.containsKey(name)) {
			String msg = String.format("Parameter '%s' was not declared.", name);
			logger.error(msg);
			throw new InvalidParameterException(msg);
		}
		return hsmParameters.get(name);
	}
}
